第14章 实践项目开发——智能温控系统
本章首先介绍实际项目开发中的一些技巧和规范性的内容,把一些实际开发中经常使用的零散知识点逐一的讲解,然后做一个温控器的小项目,模拟一个空调产品的程序控制功能,把项目开发的整个流程都走一遍,学习一下真正的项目流程。
本章内容非常重要,是否把本章内容消化理解是检验是否能够真正独立开发产品的重要的一步。实际上复杂的内容只是简单内容的堆叠,相信只要耐心认真反复学习实践,肯定可以越过龙门,步入电子工程师的行列。
14.1复合数据类型
在前边学数据类型的时候,主要是字符型、整型、浮点型等基本类型,而学数组的时候,数组的定义要求数组元素必须是相同的数据类型。在实际应用中,有时候还需要把不同类型的数据组成一个有机的整体来处理,这些组合在一个整体中的数据之间还有一定的联系,比如一个学生的姓名、性别、年龄、考试成绩等,这就引入了复合数据类型。复合数据类型主要包含结构体数据类型、共用体数据类型和枚举体数据类型。
14.1.1结构体数据类型
Kingst51开发板上的LED显示有三部分,LED小灯、数码管和LED点阵。LED小灯是1个字节控制;数码管是6个字节控制;LED点阵是8个字节控制。它们都受P0口输出控制,控制方式有相似性,可以统一进行动态扫描刷新;这三者又属于独立的LED功能显示,要改变其显示内容需要单独分别赋值。三者需要允许一起处理,也允许独立操作。如果分别进行变量定义,一是操作容易出错,二是比较零散,可读性不强。 于是,就可以使用结构体来将这一组彼此相关的数据做一个封装,既组成了一个整体,易读不易错;又可以独立单独处理其中某一个元素,这就是结构体数据类型。结构体是一种构造而成的数据类型,使用之前需要先定义它。
声明结构体变量的一般格式如下:
struct 结构体名
{
类型1 变量名1;
类型2 变量名2;
……
类型n 变量名n;
} 结构体变量名1, 结构体变量名2, ... 结构体变量名n;
这种声明方式是在声明结构体类型的同时又用它定义了结构体变量,此时的结构体名是可以省略的,但如果省略后,就不能在别处再次定义这样的结构体变量了。这种方式把类型定义和变量定义混在了一起,降低了程序的灵活性和可读性,因此并不建议采用这种方式,而是推荐用以下这种方式:
struct 结构体名
{
类型1 变量名1;
类型2 变量名2;
……
类型n 变量名n;
};
struct 结构体名 结构体变量名1, 结构体变量名2, ... 结构体变量名n;
为了方便理解,首先定义一个变量unsigned char keyBuff;然后将LED小灯、LED数码管和LED点阵,构建一个结构体类型。
struct sLedBuff { //LED显示缓冲区结构
uint8 array[8]; //点阵缓冲区
uint8 number[6]; //数码管缓冲区
uint8 single; //独立LED缓冲区
};
struct sLedBuff ledBuff;
struct
是结构体类型关键字,sLedBuff
是这个结构体名字。一旦结构体构建完成,sLedBuff
起到的作用和unsigned char
类似,是表达变量类型的。ledBuff
和keyBuff
类似,即定义了一个sLedBuff
类型的结构体变量。如果要给结构体变量的成员赋值的话,写法是`
ledBuff.single = 0xFF;
ledBuff.array[0] = 0xFF;
一个指针变量如果指向了一个结构体变量的时候,称之为结构指针变量。结构指针变量是指向的结构体变量的首地址,通过结构体指针也可以访问到这个结构变量。 结构指针变量声明的一般形式如下:
struct sLedBuff *pledBuff;
这里要特别注意的是,使用结构体指针对结构体成员的访问,和使用结构体变量名对结构体成员的访问,其表达式有所不同。结构体指针对结构体成员的访问表达式为
pledBuff ->single= 0xFF; 或者是
(*pledBuff).single= 0xFF;
很明显前者更简洁,所以推荐使用前者。
14.1.2共用体数据类型
共用体也称之为联合体,共用体定义和结构体十分类似,使用以下形式:
union 共用体名
{
数据类型1 成员名1;
数据类型2 成员名2;
……
数据类型n 成员名n;
};
union 共用体名 共用体变量;
共用体表示的是几个变量共用一个内存位置,也就是成员1、成员2……成员n都用一个内存位置。共用体成员的访问方式和结构体是一样的,成员访问的方式是:共用体名.成员名,使用指针来访问的方式是:共用体名->成员名。
从程序安全性和可移植性角度考虑,除非用户非常了解所使用的开发环境的实现细节,否则使用共用体存在代码上的隐患,所以现在诸多以安全为首要诉求的C语言编程规范禁止使用共用体。因此共用体不推荐使用,也不再赘述。
14.1.3枚举数据类型
在实际问题中,有些变量的取值被限定在一个有限的范围内。例如,一个星期从周一到周日有7天,一年从1月到12月有12个月,蜂鸣器有响和不响两种状态等等。如果把这些变量定义成整型或者字符型不是很合适,因为这些变量都有自己的范围。C语言提供了一种称为“枚举”的类型,在枚举类型的定义中列举出所有可能的值,并可以为每一个值取一个形象化的名字,它的这一特性可以提高程序代码的可读性。
枚举的说明形式如下:
enum 枚举名
{
标识符1[=整型常数],
标识符2[=整型常数],
……
标识符n[=整型常数]
};
enum 枚举名 枚举变量;
枚举的说明形式中,如果没有被初始化,那么“=整型常数”是可以被省略的,如果是默认值的话,从第一个标识符顺序赋值0、1、2……,但是当枚举中任何一个成员被赋值后,它后边的成员按照依次加1的规则确定数值。
比如本章的温控器系统,正常工作时一共有三种状态,分别为正常工作状态,设置继电器动作状态和设置报警器状态三种,可以定义一个枚举类型,根据枚举的定义E_NORMAL
值为0,E_SET_ACT
值为1,E_SET_ALARM
值为2,具体应用后续有例程。
enum eStaSystem { //系统运行状态枚举
E_NORMAL, E_SET_ACT, E_SET_ALARM
};
枚举的使用,有几点要注意:
- 枚举中每个成员结束符是逗号,而不是分号,最后一个成员可以省略逗号。
- 枚举成员的初始化值可以是负数,但是后边的成员依然依次加1。
- 枚举变量只能取枚举结构中的某个标识符常量,不可以在范围之外。
14.2类型说明
C语言不仅提供了丰富的数据类型给使用,而且还允许用户自己定义类型说明符,也就是说为了方便,给已经存在的数据类型起个“代号
”,比如“9527就是你的终身代号
”,就用9527来代表某个人。在C语言中,使用typedef
即可完成这项功能,定义格式如下:
typedef 原类型名 新类型名
typedef
语句并未定义一种新的数据类型,它仅仅是给已有的数据类型取了一个更加简洁形象的名字,可以用这个新的类型名字来定义变量。在实际开发中,很多公司都会使用这个关键字来给变量类型取新名字,一是为了方便代码的移植,还有就是可以使代码更加的简洁易读,比如以下的这几种类型定义方式。
typedef signed char int8; // 8位有符号整型数
typedef signed int int16; //16位有符号整型数
typedef signed long int32; //32位有符号整型数
typedef unsigned char uint8; // 8位无符号整型数
typedef unsigned int uint16; //16位无符号整型数
typedef unsigned long uint32; //32位无符号整型数
经过以上的这种类型说明后,今后在程序中就可以直接使用uint8
来替代unsigned char
定义变量了。聪明的你,是否发现起的这个代号的含义呢,无符号型的前边带一个u
,有符号的不带u
,int
表示整数的意思,后边的数字代表的是这个变量类型占的位数,这种命名方式很多公司都采用。
14.3头文件
在前边的章节中,多次使用过文件包含命令#include
,这条指令的功能是将指定的被包含文件的全部内容插到该命令行的位置处,从而把指定文件和当前的源程序文件连成一个源文件参与编译,通常的写法有以下两种如下:
#include <文件名>
#include “文件名”
使用尖括号表示预处理程序直接到系统指定的“包含文件目录”去查找,使用双引号则表示预处理程序首先在当前文件所在的文件目录中查找被包含的文件,如果没有找到才会再到系统的“包含文件目录”去查找。一般情况下的习惯是系统提供的头文件用尖括号方式,用户自己编写的头文件用双引号方式。
在前边用过很多次#include <reg52.h>
,这个文件所在的位置是Keil软件安装目录的\C51\INC这个路径内。在这个文件夹内,有很多系统自带的头文件,当然也包含了<intrins.h>
这个头文件。当一旦写了#include <reg52.h>
这条指令后,那么相当于在当前的.c文件中,写下了以下的代码。
#ifndef __REG52_H__
#define __REG52_H__
/* BYTE Registers */
sfr P0 = 0x80;
sfr P1 = 0x90;
sfr P2 = 0xA0;
sfr P3 = 0xB0;
... ...
/* BIT Registers */
/* PSW */
sbit CY = PSW^7;
sbit AC = PSW^6;
sbit F0 = PSW^5;
sbit RS1 = PSW^4;
sbit RS0 = PSW^3;
sbit OV = PSW^2;
sbit P = PSW^0; //8052 only
/* TCON */
sbit TF1 = TCON^7;
sbit TR1 = TCON^6;
sbit TF0 = TCON^5;
sbit TR0 = TCON^4;
sbit IE1 = TCON^3;
sbit IT1 = TCON^2;
sbit IE0 = TCON^1;
sbit IT0 = TCON^0;
... ...
#endif
之前在程序中,只要写了#include <reg52.h>
这条语句,就可以随便使用P0
、TCON
、TMOD
这些寄存器和TR0
、TR1
、TI
、RI
等这些寄存器的位,都是因为已经在这个头文件中定义或声明过了。
Keil做了很多函数,生成了库文件,如果要使用这些函数的时候,不需要再去写这些函数的代码,而直接调用这些函数即可,只是调用之前首先要进行声明,而这些声明也放在头文件当中。比如所用的_nop_();
函数,就是在<intrins.h>
这个头文件中的。
同样的,用户很多程序文件中的所要用到的函数,是在其它文件中定义的,在当前文件中要调用它们的时候,也需要提前进行外部声明。为了使程序的易维护性和可移植性提高,通常用户会自己编写所需要的头文件。用户编写的头文件中不仅仅可以进行函数的外部声明和变量的外部声明,一些宏定义也可以放在其中。
举个例子,比如在写main.c
这个文件时,配套写一个main.h
文件。新建头文件的方式也很简单,和.c是类似的,首先点击新建文件的那个图标,或者点击菜单File->New
,然后点击保存文件,保存的时候命名为main.h
即可。为了方便编写和修改维护,在Keil编程环境中新建一个头文件组,把所有的源文件放在一个组内,把所有的头文件放在一个组内,如图14-1所示。
main.h里包含了main.c所要使用的一些宏,还有对main.c内的自定义类型、全局变量、全局函数等需要提供给其他.c文件使用的内容,进行外部声明。比如把main.h文件写成下边这样。
/* 温度相关参数,温度数值左移4位是为与DS18B20数据格式保持一致 */
#define ACT_TEMP_ADDR 0x30 //继电器动作温度的E2存储地址
#define ACT_TEMP_MIN (20<<4) //继电器动作温度有效范围最小值
#define ACT_TEMP_MAX (30<<4) //继电器动作温度有效范围最大值
#define ACT_TEMP_DEFAULT (25<<4) //继电器动作温度默认值
#define ALARM_TEMP_ADDR 0x32 //高温报警温度的E2存储地址
#define ALARM_TEMP_MIN (25<<4) //高温报警温度有效范围最小值
#define ALARM_TEMP_MAX (35<<4) //高温报警温度有效范围最大值
#define ALARM_TEMP_DEFAULT (30<<4) //高温报警温度默认值
/* 全局数据类型定义 */
enum eStaSystem { //系统运行状态枚举
E_NORMAL, E_SET_ACT, E_SET_ALARM
};
void TempControl();
void KeyAction(uint8 keycode);
void ConfigTimer0(uint16 ms);
请注意,如果对函数进行外部声明,extern
是可以省略的;如果还有外部变量需要进行声明,extern
是不能省略的。为了确保程序的可靠性和可移植性,尽量不使用外部变量,不同文件之间尽量采用函数传递信息。
头文件这样编写看似没问题,实际上则不然。在程序编写过程中,经常会遇到头文件包含头文件的用法,假设a.h包含了main.h
文件,b.h
文件同样也包含了main.h
文件,如果现在有一个c文件x.c
,它既包含了a.h
又包含了b.h
,这样就会出现头文件main.h
被x.c
重复包含了,从而会发生变量函数等的重复声明,因此还得用到C语言的另一个知识点——条件编译。
14.4条件编译
条件编译属于预处理程序,包括之前讲的宏,都是程序在编译之前做的一些必要的处理,这些都不是实际程序功能代码,而仅仅是告诉编译器需要进行的特定操作等。
条件编译通常有三种用法,第一种表达式:
#if 表达式
程序段 1
#else
程序段 2
#endif
作用:如果表达式的值为“真”(非0),则编译程序段1,否则,编译程序段2。在使用中,表达式通常是一个常量,事先用宏来进行声明,通过宏声明的值来确定到底执行哪段程序。 比如公司开发了同类的两款产品,这两款产品的功能有一部分是相同的,有一部分是不同的,同样所编写的程序大部分的代码是一样的,只有少部分有区别。这个时候为了方便程序的维护,可以把两款产品的代码写到同一个工程程序中,然后把其中有区别的功能利用条件编译。
#define PLAN 0
#if (PLAN == 0)
程序段1
#else
程序段2
#endif
这样写之后,当要编译款式1的时候,把PLAN宏声明成0即可,当要编译款式2的时候,把宏声明的值改为1或其它值即可。
第二种表达式和第三种表达式是类似的,使用哪一种要看具体情况或个人偏好。
表达式二:
#ifdef 标识符
程序段1
#else
程序段2
#endif
表达式三:
#ifndef 标识符
程序段1
#else
程序段2
#endif
在本章的示例中使用到了表达式三,表达式三的作用是:如果标识符没有被#define
命令所声明过,则编译程序段1,否则则编译程序段2。此外,命令中的#else部分是可以省略的。表达式二和表达式三正好相反,实际#ifndef
就是if no define的缩写。
在头文件的编写过程中,为了防止命名的错乱,每个.c
文件对应的.h
文件内的条件编译的命名,也使用这个头文件的名字,并且大写,在中间加上下划线,比如这个main.h
的结构,首先要这样写:
#ifndef _MAIN_H /*本段程序是写在main.h文件中,程序段1为外部变量;外部函数;自定义数据类型等*/
#define _MAIN_H
程序段1
#endif
这样说明的意思是,如果这个_MAIN_H
没有声明过,那么就声明_MAIN_H
,并且的程序段1是有效的,最终结束;那么如果_MAIN_H
已经声明过了,那么也就不用再声明了,同时程序段1也就无效了。
前边头文件重复包含的问题这样就被解决了,编译器在预编译a.h时进行了#define _MAIN_H
,而在预编译b.h
时,#ifndef _MAIN_H
这一个条件就不成立,也就不会编译b.h
当中的这部分内容,这样就有效的解决了头文件重复包含的问题。
14.5项目实战
开始进入本章的重头戏,也是本书即将结束的综合训练,实实在在的实战项目--智能温控系统。当接到一个具体项目开发任务后,要根据项目做出框架规划,整理出逻辑思路,设计电路图,写出规范的代码,调试代码最终完成功能。
14.5.1项目需求分析
作为一个空调的温控器,基本功能以及使用Kingst51开发板上的电子元件包含:
- 显示当前室内温度,使用18B20温度传感器和数码管完成。
- 记录当前设定的温度值,以便下次开机直接使用,使用EEPROM完成。
- 当室内温度高于设定温度时,启动压缩机开始工作,使用继电器完成。
- 当空调压缩机损坏,无法降低室内温度,温度高到一定值,系统报警,使用蜂鸣器完成。
- 手动调整制冷温度值和报警温度值,使用按键和数码管和LED小灯完成。
- 使用LED点阵显示一个爱心图像。
14.5.2硬件电路布局和选型
设计一个产品,并不是一个所需功能的简单杂乱堆积,而是要从功能设计,产品布局让人用起来更舒服、更愉悦、更符合人性化,这才是一个优秀的设计。比如以Kingst51开发板作为智能温控器的设计参考产品,硬件设计和开发充分考虑的几个方面。
- 主控芯片通常放到整个线路板的偏中心位置,方便走线。
- 显示部分的LED小灯、数码管和点阵都放到线路板右上方比较醒目的位置。
- 按键输入部分,放到线路板的右下角,方便右手按按键。
- 继电器、蜂鸣器放到左上方;USB供电和通信放到左下方。
对于器件的选择,考虑的因素有:
- 所选器件是否可靠和稳定,根据产品的定位考量。如果是DIY练习为主,器件选择可以能方便购买就行;如果是批量产品,要充分考虑器件品质,能否耐用,长期使用,尽量不选一些最近半年出的新的器件。
- 所选器件是否满足批量生产、长期生产的需求。首先避免所选器件为市场淘汰产品,市场上仅有一些二手拆机件,容易造成整个产品次品率大幅提高,生产供应跟不上。其次要确保所选器件厂家的品质和实力,能否确保长期使用不断货。
- 选择器件时考虑部分器件是否方便替代,是否可以有备选方案。
- 所选器件是否方便生产。原则上能用SMT贴片的,就不选插件的;能选机器完成焊接的,就不能选人工组装的。
- 满足稳定可靠的基础上,选择价格合理的,控制整体产品的硬件成本。
- 最后还可以有美观上的加分项,比如Kingst51开发板的器件,整个板子设计成红色,需要手动操作的部件包括插针都选了蓝色。
14.5.3程序结构规划
项目需求和硬件规划已经确定,那就要开始研究如何实现所需要的软件功能,程序结构如何组织。一个项目,如果需要参与工作的器件比较多,同时实现的功能也很多,为了方便程序编写和维护,整个程序必须采用模块化编程,也就是每个功能模块对应一个.c文件来实现,这种用法在前边的章节已经开始使用了。一方面,所有的代码堆到一起会显得杂乱无章,更重要的是容易造成意外错误,程序一旦有逻辑上的问题或者功能增加或者程序更新的需求,修改程序代码将是一种灾难。另外一方面,如果一个项目程序很大的时候,可能需要多个程序开发人员共同参与编程,多模块的方式也可以让每位程序员之间的代码最终很灵活的融合到一起。
模块的划分没有什么教条可以遵循,而是根据具体需要灵活处理,就以这个智能温控器项目为例。这个项目在实际开发中算是一个微型产品,所以功能和模块规划也比较简单。
- LED小灯、数码管和LED点阵,都是显示部件,因此合并一个功能模块led.c。
- 矩阵按键驱动部分程序代码,作为单独的功能模块keyboard.c。
- 温度传感器作为一个独立的功能模块DS18b20.c。
- 虽然EEPROM和I2C在本项目中是一起用的,但是依然做成了两个模块,方便做成通用的程序用来移植。
- 系统工作模式分为三种模式:正常工作模式,设置继电器动作温度和设置蜂鸣器报警温度,主程序不断地扫描目前系统所处的模式,扫描按键并执行相应的动作,同时主程序定时扫描进行温度显示。
随着程序量逐步增多,程序功能逐步增强,对于程序的划分需要逐步有分层的思路。其中按键、LED、I2C、EEPROM、18B20这些都属于底层驱动的范畴,它们要共同为上层应用服务,那么上层是什么呢?就是根据最终需要显示的效果来调度各种驱动函数,根据按键的输入来实现模式切换和温度调整,并且控制系统进行继电器动作和蜂鸣器报警动作,这些都属于应用层的功能。
14.5.4程序代码编写
在实际项目开发中,不仅仅希望源程序、头文件等文件结构规范、代码编写规范,还希望工程文件规整规范,方便维护。因此首先新建一个lesson14_1的文件夹,用来存放本章的工程文件。而后新建工程保存的时候,在lesson14-1文件夹内再建立一个文件夹,取名为project,专门用于存放工程文件的,如图14-2所示。
然后新建文件进行保存时,在lesson14_1目录下再建立一个文件夹,取名为source文件夹,专门用来存放源代码,如图14-3所示。
看之前的工程例子能看到,工程编译后会生成很多额外的文件,这些文件可以统称为编译输出文件。在lesson14_1目录下再建立一个output文件夹来存放这些文件。要改变输出文件的路径,需要修改两处:首先进入Options for Target,选择Output选项页,点击Select Folder for Objects,在弹出的对话框中选择新建的output文件夹;然后再进入Listing选项页,点击Select Folder for Listings,同样指定output文件夹。
工程建立完毕,文件夹也整理妥当,下面就开始正式编写代码。当要进行一个实际产品或者项目开发的时候,首先电路原理图是确定的,所使用的单片机的引脚也是明确的,还有一些比如类型说明,一些特殊的全局参数及宏声明放到一个专门的头文件中,在这里命名为config.h,即全局的配置文件。
/****************************config.h文件程序源代码*****************************/
#ifndef _CONFIG_H
#define _CONFIG_H
/* 通用头文件 */
#include <reg52.h>
#include <intrins.h>
/* 数据类型定义 */
typedef signed char int8; // 8位有符号整型数
typedef signed int int16; //16位有符号整型数
typedef signed long int32; //32位有符号整型数
typedef unsigned char uint8; // 8位无符号整型数
typedef unsigned int uint16; //16位无符号整型数
typedef unsigned long uint32; //32位无符号整型数
/* 全局运行参数定义 */
#define SYS_MCLK (11059200/12) //系统主时钟频率,即振荡器频率÷12
/* 全局数据类型定义 */
enum eStaSystem { //系统运行状态枚举
E_NORMAL, E_SET_ACT, E_SET_ALARM
};
/* IO引脚分配定义 */
sbit KEY_IN_1 = P2^4; //矩阵按键的扫描输入引脚1
sbit KEY_IN_2 = P2^5; //矩阵按键的扫描输入引脚2
sbit KEY_IN_3 = P2^6; //矩阵按键的扫描输入引脚3
sbit KEY_IN_4 = P2^7; //矩阵按键的扫描输入引脚4
sbit KEY_OUT_1 = P2^3; //矩阵按键的扫描输出引脚1
sbit KEY_OUT_2 = P2^2; //矩阵按键的扫描输出引脚2
sbit KEY_OUT_3 = P2^1; //矩阵按键的扫描输出引脚3
sbit KEY_OUT_4 = P2^0; //矩阵按键的扫描输出引脚4
sbit ADDR0 = P1^0; //LED位选译码地址引脚0
sbit ADDR1 = P1^1; //LED位选译码地址引脚1
sbit ADDR2 = P1^2; //LED位选译码地址引脚2
sbit ADDR3 = P1^3; //LED位选译码地址引脚3
sbit ENLED = P1^4; //LED显示部件的总使能引脚
sbit I2C_SCL = P3^7; //I2C总线时钟引脚
sbit I2C_SDA = P3^6; //I2C总线数据引脚
sbit BUZZ = P1^6; //蜂鸣器控制引脚
sbit RELAY = P3^3; //继电器控制引脚
sbit IO_18B20 = P3^2; //DS18B20通信引脚
#endif
这个config.h中包含了系统所共同使用的类型声明以及宏声明,方便使用。下边的编程步骤,就是从main.c文件整体框架开始。
作为资深的研发工程师调试这样一个程序,也得几个小时的时间,不可能写出来就好用,所以在这里无法把整个过程给大家还原出来,但是主要的编写代码的过程会尽可能的给大家介绍一下。
程序的流程虽然是从main.c开始的,但编写代码,往往用主程序的流程框架作为主线,逐一对每一个单独的功能模块进行调试验证。
习惯上,首先调试显示LED显示程序。因为调试好了显示程序后,再调试其他任何模块,都可以通过LED显示出来结果,用来确认每个模块的功能是否正常。
讲结构体的时候有讲到,将小灯、数码管和点阵这三种器件的控制构建sLedBuff 这样一个结构体,用这个结构体类型定义了一个统一的显示缓冲区ledBuff,那么在动态扫描的中断函数中刷新程序语句,只需要用这样一行代码P0 = *((uint8 *)&ledBuff+i)
。这行代码首先将ledBuff的地址强制类型转换uint8类型指针,这个作用是确保访问的内存区域的大小和类型,防止内存越界访问或者类型混淆。然后取自这个指针起的第i个字节的数据送给P0,这样就完成了字节为单位的小灯、数码管和点阵这三种LED的全部动态扫描刷新。
/***************************Led.h文件程序源代码********************************/
#ifndef _LED_H
#define _LED_H
#include "config.h"
struct sLedBuff { //LED显示缓冲区结构
uint8 array[8]; //点阵缓冲区
uint8 number[6]; //数码管缓冲区
uint8 single; //独立LED缓冲区
};
void InitLed();
void LedScan();
void ShowSystemSta(enum eStaSystem sta);
void ShowTempValue(int16 temp);
void ShowLedImage(uint8 show);
#endif
/***************************Led.c文件程序源代码********************************/
#include "Led.h"
uint8 code LedChar[] = { //数码管显示字符转换表
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
uint8 code LedImage[] = { //点阵图像(爱心)
0xFF, 0x99, 0x00, 0x00, 0x00, 0x81, 0xC3, 0xE7
};
struct sLedBuff ledBuff; //LED显示缓冲区,默认初值全0,正好达到上电全亮的效果
/* LED初始化函数,初始化IO、配置定时器 */
void InitLed()
{
P0 = 0xFF;
ENLED = 0;
ADDR3 = 1;
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 1;
}
/* 用独立LED显示系统状态 */
void ShowSystemSta(enum eStaSystem sta)
{
if (sta == E_SET_ACT)
ledBuff.single = 0xFE; //设置继电器动作温度时点亮最低位LED
else if (sta == E_SET_ALARM)
ledBuff.single = 0xFD; //设置高温报警温度时点亮次低位LED
else
ledBuff.single = 0xFF; //其它状态时LED全熄灭
}
/* 数码管上显示一位数字,index-数码管位索引(从右到左对应0~5),
** num-待显示的数字,point-代表是否显示此位上的小数点 */
void LedNumber(uint8 index, uint8 num, uint8 point)
{
ledBuff.number[index] = LedChar[num]; //输入数字转换为数码管字符0~F
if (point != 0)
{
ledBuff.number[index] &= 0x7F; //point不为0时点亮当前位的小数点
}
}
/* 在数码管上显示温度值,温度值格式为DS18B20数据格式 */
void ShowTempValue(int16 temp)
{
uint8 i;
uint8 sign; //温度值的符号位
int16 intT, decT; //温度值的整数和小数部分
if (temp >= 0) //温度为正数时,记录符号位,直接分离出整数和小数部分
{
sign = 0;
intT = temp >> 4;
decT = temp & 0xF;
}
else //温度为负数时,记录符号位,温度转为正数再分离出整数和小数部分
{
sign = 1;
intT = (-temp) >> 4;
decT = (-temp) & 0xF;
}
//处理小数部分的显示
decT = (decT*10) / 16; //二进制的小数部分转换为1位十进制位
LedNumber(0, decT, 0); //显示小数位
//处理整数部分的显示
LedNumber(1, intT%10, 1); //显示整数个位+小数点
for (i=2; i<6; i++) //用循环检测更高位数值
{
intT /= 10;
if (intT > 0) //不为0时,显示该位数值
LedNumber(i, intT%10, 0);
else //为0时,则跳出循环
break;
}
//处理符号位的显示
if (sign == 1)
{
ledBuff.number[i++] = 0xBF;
}
//更高位清空
for ( ; i<6; i++)
{
ledBuff.number[i] = 0xFF;
}
}
/* 显示点阵图像,show-显示或清除图像 */
void ShowLedImage(uint8 show)
{
uint8 i;
if (show)
{
for (i=0; i<8; i++)
ledBuff.array[i] = LedImage[i];
}
else
{
for (i=0; i<8; i++)
ledBuff.array[i] = 0xFF;
}
}
/* LED动态扫描函数,在定时中断中调用 */
void LedScan()
{
static uint8 i = 0; //LED位选索引
P0 = 0xFF; //关闭所有段选位,显示消隐
P1 = (P1 & 0xF0) | i; //位选索引值赋值到P1口低4位
P0 = *((uint8*)&ledBuff+i); //缓冲区中索引位置的数据送到P0口
if (i < (sizeof(ledBuff)-1)) //索引递增循环,遍历整个缓冲区
i++;
else
i = 0;
}
使用EEPROM的作用主要是记忆继电器动作的温度值和高温报警温度值,关于I2C和EEPROM的程序代码,和前边课程讲过的基本一致。
/***************************I2C.h文件程序源代码********************************/
#ifndef _I2C_H
#define _I2C_H
#include "config.h"
#define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}
void I2CStart();
void I2CStop();
uint8 I2CReadNAK();
uint8 I2CReadACK();
bit I2CWrite(uint8 dat);
#endif
/***************************I2C.c文件程序源代码********************************/
#include "I2C.h"
/* 产生总线起始信号 */
void I2CStart()
{
I2C_SDA = 1; //首先确保SDA、SCL都是高电平
I2C_SCL = 1;
I2CDelay();
I2C_SDA = 0; //先拉低SDA
I2CDelay();
I2C_SCL = 0; //再拉低SCL
}
/* 产生总线停止信号 */
void I2CStop()
{
I2C_SCL = 0; //首先确保SDA、SCL都是低电平
I2C_SDA = 0;
I2CDelay();
I2C_SCL = 1; //先拉高SCL
I2CDelay();
I2C_SDA = 1; //再拉高SDA
I2CDelay();
}
/* I2C总线写操作,dat-待写入字节,返回值-从机应答位的值 */
bit I2CWrite(unsigned char dat)
{
bit ack; //用于暂存应答位的值
unsigned char mask; //用于探测字节内某一位值的掩码变量
for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行
{
if ((mask&dat) == 0) //该位的值输出到SDA上
I2C_SDA = 0;
else
I2C_SDA = 1;
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
I2C_SCL = 0; //再拉低SCL,完成一个位周期
}
I2C_SDA = 1; //8位数据发送完后,主机释放SDA,以检测从机应答
I2CDelay();
I2C_SCL = 1; //拉高SCL
ack = I2C_SDA; //读取此时的SDA值,即为从机的应答值
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成应答位,并保持住总线
return (~ack); //应答值取反以符合通常的逻辑:
//0=不存在或忙或写入失败,1=存在且空闲或写入成功
}
/* I2C总线读操作,并发送非应答信号,返回值-读到的字节 */
unsigned char I2CReadNAK()
{
unsigned char mask;
unsigned char dat;
I2C_SDA = 1; //首先确保主机释放SDA
for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行
{
I2CDelay();
I2C_SCL = 1; //拉高SCL
if(I2C_SDA == 0) //读取SDA的值
dat &= ~mask; //为0时,dat中对应位清零
else
dat |= mask; //为1时,dat中对应位置1
I2CDelay();
I2C_SCL = 0; //再拉低SCL,以使从机发送出下一位
}
I2C_SDA = 1; //8位数据发送完后,拉高SDA,发送非应答信号
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成非应答位,并保持住总线
return dat;
}
/* I2C总线读操作,并发送应答信号,返回值-读到的字节 */
unsigned char I2CReadACK()
{
unsigned char mask;
unsigned char dat;
I2C_SDA = 1; //首先确保主机释放SDA
for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行
{
I2CDelay();
I2C_SCL = 1; //拉高SCL
if(I2C_SDA == 0) //读取SDA的值
dat &= ~mask; //为0时,dat中对应位清零
else
dat |= mask; //为1时,dat中对应位置1
I2CDelay();
I2C_SCL = 0; //再拉低SCL,以使从机发送出下一位
}
I2C_SDA = 0; //8位数据发送完后,拉低SDA,发送应答信号
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成应答位,并保持住总线
return dat;
}
/**************************eeprom.h文件程序源代码*******************************/
#ifndef _EEPROM_H
#define _EEPROM_H
#include "I2C.h"
void E2Read(uint8 *buf, uint8 addr, uint8 len);
void E2Write(uint8 *buf, uint8 addr, uint8 len);
#endif
/**************************eeprom.c文件程序源代码*******************************/
#include "eeprom.h"
/* E2读取函数,buf-数据接收指针,addr-E2中的起始地址,len-读取长度 */
void E2Read(unsigned char *buf, unsigned char addr, unsigned char len)
{
do { //用寻址操作查询当前是否可进行读写操作
I2CStart();
if (I2CWrite(0x50<<1)) //应答则跳出循环,非应答则进行下一次查询
{
break;
}
I2CStop();
} while(1);
I2CWrite(addr); //写入起始地址
I2CStart(); //发送重复启动信号
I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作
while (len > 1) //连续读取len-1个字节
{
*buf++ = I2CReadACK(); //最后字节之前为读取操作+应答
len--;
}
*buf = I2CReadNAK(); //最后一个字节为读取操作+非应答
I2CStop();
}
/* E2写入函数,buf-源数据指针,addr-E2中的起始地址,len-写入长度 */
void E2Write(unsigned char *buf, unsigned char addr, unsigned char len)
{
while (len > 0)
{
//等待上次写入操作完成
do { //用寻址操作查询当前是否可进行读写操作
I2CStart();
if (I2CWrite(0x50<<1)) //应答则跳出循环,非应答则进行下一次查询
{
break;
}
I2CStop();
} while(1);
//按页写模式连续写入字节
I2CWrite(addr); //写入起始地址
while (len > 0)
{
I2CWrite(*buf++); //写入一个字节数据
len--; //待写入长度计数递减
addr++; //E2地址递增
if ((addr&0x07) == 0) //检查地址是否到达页边界,24C02每页8字节,
{ //所以检测低3位是否为零即可
break; //到达页边界时,跳出循环,结束本次写操作
}
}
I2CStop();
}
}
程序用到了矩阵按键,矩阵按键的驱动程序,也可以直接移植之前的程序代码。
/************************keyboard.h文件程序源代码******************************/
#ifndef _KEY_BOARD_H
#define _KEY_BOARD_H
#include "config.h"
void KeyScan();
void KeyDriver();
#endif
/************************keyboard.c文件程序源代码******************************/
#include "keyboard.h"
#include "main.h"
const uint8 code KeyCodeMap[4][4] = { //矩阵按键到标准键码的映射表
{ '1', '2', '3', 0x26 }, //数字键1、数字键2、数字键3、向上键
{ '4', '5', '6', 0x25 }, //数字键4、数字键5、数字键6、向左键
{ '7', '8', '9', 0x28 }, //数字键7、数字键8、数字键9、向下键
{ '0', 0x1B, 0x0D, 0x27 } //数字键0、ESC键、 回车键、 向右键
};
uint8 KeySta[4][4] = { //全部矩阵按键的当前状态
{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}
};
/* 按键驱动函数,检测按键动作,调度相应动作函数,需在主循环中调用 */
void KeyDriver()
{
uint8 i, j;
static uint8 backup[4][4] = { //按键值备份,保存前一次的值
{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}
};
for (i=0; i<4; i++) //循环检测4*4的矩阵按键
{
for (j=0; j<4; j++)
{
if (backup[i][j] != KeySta[i][j]) //检测按键动作
{
if (backup[i][j] != 0) //按键按下时执行动作
{
KeyAction(KeyCodeMap[i][j]); //调用按键动作函数
}
backup[i][j] = KeySta[i][j]; //刷新前一次的备份值
}
}
}
}
/* 按键扫描函数,需在定时中断中调用,推荐调用间隔1ms */
void KeyScan()
{
uint8 i;
static uint8 keyout = 0; //矩阵按键扫描输出索引
static uint8 keybuf[4][4] = { //矩阵按键扫描缓冲区
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}
};
switch (keyout) //根据输出索引值,拉低相应输出引脚
{
case 0: KEY_OUT_1 = 0; break;
case 1: KEY_OUT_2 = 0; break;
case 2: KEY_OUT_3 = 0; break;
case 3: KEY_OUT_4 = 0; break;
default: break;
}
//将一行的4个按键值移入缓冲区
keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1;
keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2;
keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3;
keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4;
//消抖后更新按键状态
for (i=0; i<4; i++) //每行4个按键,所以循环4次
{
if ((keybuf[keyout][i] & 0x0F) == 0x00)
{ //连续4次扫描值为0,即4*4ms内都是按下状态时,可认为按键已稳定的按下
KeySta[keyout][i] = 0;
}
else if ((keybuf[keyout][i] & 0x0F) == 0x0F)
{ //连续4次扫描值为1,即4*4ms内都是弹起状态时,可认为按键已稳定的弹起
KeySta[keyout][i] = 1;
}
}
keyout++; //输出索引值递增
keyout &= 0x03; //索引值加到4即归零
//拉高全部输出引脚
KEY_OUT_1 = 1;
KEY_OUT_2 = 1;
KEY_OUT_3 = 1;
KEY_OUT_4 = 1;
}
DS18B20温度传感器的程序代码也可以直接将之前的程序移植过来。
/**************************DS18B20.h文件程序源代码*****************************/
#ifndef _DS18B20_H
#define _DS18B20_H
#include "config.h"
bit Start18B20();
bit Get18B20Temp(int16 *temp);
#endif
/**************************DS18B20.c文件程序源代码*****************************/
#include "DS18B20.h"
/* 软件延时函数,延时时间(t*10)us */
void DelayX10us(uint8 t)
{
do {
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
} while (--t);
}
/* 复位总线,获取存在脉冲,以启动一次读写操作 */
bit Get18B20Ack()
{
bit ack;
EA = 0; //禁止总中断
IO_18B20 = 0; //产生500us复位脉冲
DelayX10us(50);
IO_18B20 = 1;
DelayX10us(6); //延时60us
ack = IO_18B20; //读取存在脉冲
while(!IO_18B20); //等待存在脉冲结束
EA = 1; //重新使能总中断
return ack;
}
/* 向DS18B20写入一个字节,dat-待写入字节 */
void Write18B20(uint8 dat)
{
uint8 mask;
EA = 0; //禁止总中断
for (mask=0x01; mask!=0; mask<<=1) //低位在先,依次移出8个bit
{
IO_18B20 = 0; //产生2us低电平脉冲
_nop_();
_nop_();
if ((mask&dat) == 0) //输出该bit值
IO_18B20 = 0;
else
IO_18B20 = 1;
DelayX10us(6); //延时60us
IO_18B20 = 1; //拉高通信引脚
}
EA = 1; //重新使能总中断
}
/* 从DS18B20读取一个字节,返回值-读到的字节 */
uint8 Read18B20()
{
uint8 dat;
uint8 mask;
EA = 0; //禁止总中断
for (mask=0x01; mask!=0; mask<<=1) //低位在先,依次采集8个bit
{
IO_18B20 = 0; //产生2us低电平脉冲
_nop_();
_nop_();
IO_18B20 = 1; //结束低电平脉冲,等待18B20输出数据
_nop_(); //延时2us
_nop_();
if (!IO_18B20) //读取通信引脚上的值
dat &= ~mask;
else
dat |= mask;
DelayX10us(6); //再延时60us
}
EA = 1; //重新使能总中断
return dat;
}
/* 启动一次18B20温度转换,返回值-表示是否启动成功 */
bit Start18B20()
{
bit ack;
ack = Get18B20Ack(); //执行总线复位,并获取18B20应答
if (ack == 0) //如18B20正确应答,则启动一次转换
{
Write18B20(0xCC); //跳过ROM操作
Write18B20(0x44); //启动一次温度转换
}
return ~ack; //ack==0表示操作成功,所以返回值对其取反
}
/* 读取DS18B20转换的温度值,返回值-表示是否读取成功 */
bit Get18B20Temp(int16 *temp)
{
bit ack;
uint8 LSB, MSB; //16bit温度值的低字节和高字节
ack = Get18B20Ack(); //执行总线复位,并获取18B20应答
if (ack == 0) //如18B20正确应答,则读取温度值
{
Write18B20(0xCC); //跳过ROM操作
Write18B20(0xBE); //发送读命令
LSB = Read18B20(); //读温度值的低字节
MSB = Read18B20(); //读温度值的高字节
*temp = ((int16)MSB << 8) + LSB; //合成为16bit整型数
}
return ~ack; //ack==0表示操作应答,所以返回值为其取反值
}
程序主要应用层框架都在main.c 文件中。上电对各个模块初始化完毕后,对继电器动作和蜂鸣器报警的温度值做限定,防止意外错误。程序进入主循环阶段,检测按键是否被按下,切换系统状态(只用到了回车、向上和向下三个按键);检测目前的温度值,针对温度值判断是否进行温度控制动作。配置初始化定时器0,利用定时器0中断进行时间控制。
/******************************main.h文件程序源代码*****************************/
#ifndef _MAIN_H
#define _MAIN_H
#include "config.h"
/* 温度相关参数,温度数值左移4位是为与DS18B20数据格式保持一致 */
#define ACT_TEMP_ADDR 0x30 //继电器动作温度的E2存储地址
#define ACT_TEMP_MIN (20<<4) //继电器动作温度有效范围最小值
#define ACT_TEMP_MAX (30<<4) //继电器动作温度有效范围最大值
#define ACT_TEMP_DEFAULT (25<<4) //继电器动作温度默认值
#define ALARM_TEMP_ADDR 0x32 //高温报警温度的E2存储地址
#define ALARM_TEMP_MIN (25<<4) //高温报警温度有效范围最小值
#define ALARM_TEMP_MAX (35<<4) //高温报警温度有效范围最大值
#define ALARM_TEMP_DEFAULT (30<<4) //高温报警温度默认值
void TempControl();
void KeyAction(uint8 keycode);
void ConfigTimer0(uint16 ms);
#endif
/******************************main.c文件程序源代码*****************************/
#include "DS18B20.h"
#include "keyboard.h"
#include "eeprom.h"
#include "Led.h"
#include "main.h"
bit flag2s = 0; //2s定时标志位
bit staBuzz = 0; //蜂鸣器状态标志
uint8 T0RH = 0; //T0重载值的高字节
uint8 T0RL = 0; //T0重载值的低字节
int16 curTemp = 0; //当前读取的温度值
int16 actTemp = 0; //继电器动作温度设定值
int16 alarmTemp = 0; //高温报警温度设定值
enum eStaSystem staSystem = E_NORMAL; //系统运行状态
void main()
{
EA = 1; //开总中断
InitLed(); //初始化LED模块
Start18B20(); //启动首次温度转换
ConfigTimer0(1); //配置T0定时1ms
//读取继电器动作温度,超出有效范围则设置为默认值
E2Read((uint8*)&actTemp, ACT_TEMP_ADDR, sizeof(actTemp));
if ((actTemp < ACT_TEMP_MIN) || (actTemp > ACT_TEMP_MAX))
{
actTemp = ACT_TEMP_DEFAULT;
}
//读取高温报警温度,超出有效范围则设置为默认值
E2Read((uint8*)&alarmTemp, ALARM_TEMP_ADDR, sizeof(alarmTemp));
if ((alarmTemp < ALARM_TEMP_MIN) || (alarmTemp > ALARM_TEMP_MAX))
{
alarmTemp = ALARM_TEMP_DEFAULT;
}
while (!flag2s); //等待2秒
ShowSystemSta(staSystem);
ShowLedImage(0);
while (1) //进入主循环
{
KeyDriver(); //执行按键驱动
if (flag2s) //每隔2s执行以下分支
{
flag2s = 0;
if (Get18B20Temp(&curTemp)) //读取当前温度
{
if (staSystem == E_NORMAL)
{
ShowTempValue(curTemp); //刷新温度显示
TempControl(); //执行温度控制
}
}
Start18B20(); //重新启动下一次转换
}
}
}
/* 温度控制函数 */
void TempControl()
{
//检测执行执行继电器动作
if (curTemp > actTemp)
{ //高于设定温度时继电器吸合
RELAY = 0;
ShowLedImage(1);
}
else if (curTemp < actTemp)
{ //低于设定温度时继电器释放
RELAY = 1;
ShowLedImage(0);
}
//检测执行高温报警
if (curTemp >= alarmTemp)
staBuzz = 1;
else
staBuzz = 0;
}
/* 按键动作函数,根据键码执行相应的操作,keycode-按键键码 */
void KeyAction(uint8 keycode)
{
if (keycode == 0x26) //向上键,增加温度设定值
{
if (staSystem == E_SET_ACT)
{
actTemp += (1<<4);
if (actTemp > ACT_TEMP_MAX)
actTemp = ACT_TEMP_MAX;
}
else if (staSystem == E_SET_ALARM)
{
alarmTemp += (1<<4);
if (alarmTemp > ALARM_TEMP_MAX)
alarmTemp = ALARM_TEMP_MAX;
}
}
else if (keycode == 0x28) //向下键,减小温度设定值
{
if (staSystem == E_SET_ACT)
{
actTemp -= (1<<4);
if (actTemp < ACT_TEMP_MIN)
actTemp = ACT_TEMP_MIN;
}
else if (staSystem == E_SET_ALARM)
{
alarmTemp -= (1<<4);
if (alarmTemp < ALARM_TEMP_MIN)
alarmTemp = ALARM_TEMP_MIN;
}
}
else if (keycode == 0x0D) //回车键,切换运行/设置状态
{
switch (staSystem)
{
case E_NORMAL:
staSystem = E_SET_ACT;
break;
case E_SET_ACT:
E2Write((uint8*)&actTemp, ACT_TEMP_ADDR, sizeof(actTemp));
staSystem = E_SET_ALARM;
break;
case E_SET_ALARM:
E2Write((uint8*)&alarmTemp, ALARM_TEMP_ADDR, sizeof(alarmTemp));
staSystem = E_NORMAL;
TempControl();
break;
default:
break;
}
ShowSystemSta(staSystem);
}
//根据系统状态刷新温度显示
switch (staSystem)
{
case E_NORMAL: ShowTempValue(curTemp); break;
case E_SET_ACT: ShowTempValue(actTemp); break;
case E_SET_ALARM: ShowTempValue(alarmTemp); break;
default: break;
}
}
/* 配置并启动T0,ms-T0定时时间 */
void ConfigTimer0(uint16 ms)
{
uint32 tmp;
tmp = (SYS_MCLK*ms)/1000; //计算所需的计数值
tmp = 65536 - tmp; //计算定时器重载值
tmp = tmp + 33; //补偿中断响应延时造成的误差
T0RH = (uint8)(tmp>>8); //定时器重载值拆分为高低字节
T0RL = (uint8)tmp;
TMOD &= 0xF0; //清零T0的控制位
TMOD |= 0x01; //配置T0为模式1
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
}
/* T0中断服务函数,实现系统定时和按键扫描 */
void InterruptTimer0() interrupt 1
{
static uint16 tmr2s = 0;
static uint16 tmrBuzz = 0;
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
LedScan(); //LED扫描显示
KeyScan(); //矩阵按键扫描
//定时2s
tmr2s++;
if (tmr2s >= 2000)
{
tmr2s = 0;
flag2s = 1;
}
//蜂鸣器驱动
if (staBuzz) //蜂鸣器启动,实现间隔发声
{
if (tmrBuzz < 500) //0~0.5s发声
{
BUZZ = 0;
tmrBuzz++;
}
else if (tmrBuzz < 1000) //0.5~1s不发声
{
BUZZ = 1;
tmrBuzz++;
}
else //计时到1s后重新开始
{
tmrBuzz = 0;
}
}
else //蜂鸣器关闭
{
BUZZ = 1;
tmrBuzz = 0;
}
}
程序代码已经完成了,但是学习还得继续,把思路学差不多之后,要能够不看源代码,独立把这个程序编写出来,那么我就可以很高兴的告诉你,你的单片机已经合格了,你可以动手开发一些小产品,进入下一个层次的历练了。
当然了,各位读者不要指望这样的代码一下子写出来就好用,包括研发工程师,调试这种代码也是一步步来的,在调试的过程中,可能还要穿插修改很多之前写好的代码,协调功能工作等等。如果独立写这种代码,1到3天调试完成还是比较正常的。学到这里,相信各位读者对于做技术的基本耐性已经具备了。做技术,耐心、细心、恒心,缺一不可。不要像初学那样遇到一个问题动不动就浮躁了,慢慢来,最终把这个功能实现出来,完成你单片机之路的第一个项目。
14.6练习题
- 学会使用类型说明定义新类型。
- 学会建立编写头文件,并且掌握头文件的格式。
- 掌握条件编译的用法。
- 独立将智能温控器项目开发的代码完成。